<?php

namespace framework\nosql;

use framework\core\Config;

/**
 *  Redis 缓存驱动
 *
 * 要求安装phpredis扩展：https://github.com/phpredis/phpredis/
 *
 */
class Redis {

    private $conf;
    private $group = '_cache_';
    private $prefix = 'guipin_';
    private $ver = 0;
    private $link = [];
    private $hash;

    /**
     * 是否连接server
     */
    private $isConnected = false;

    /**
     * 重连次数
     * @var int
     */
    private $reConnected = 0;

    /**
     * 最大重连次数,默认为2次
     * @var int
     */
    private $maxReConnected = 2;

    public function __construct(string $conf_name = 'redis_cache') {

        if (!extension_loaded('redis')) {
            throw new \Exception('当前环境不支持: redis');
        }

        $this->conf = Config::getInstance()->get($conf_name);

        if (empty($this->conf)) {
            throw new \Exception('请配置 redis !');
        }

        $this->connect();
    }

    /**
     * 　单实例化
     * @staticvar array $obj
     * @param type $conf_name
     * @return \self
     */
    public static function getInstance(string $conf_name = 'redis_cache') {
        static $obj = [];
        if (!isset($obj[$conf_name])) {
            $obj[$conf_name] = new self($conf_name);
        }
        return $obj[$conf_name];
    }

    private function connect() {

        $this->hash = new Flexihash();

        foreach ($this->conf as $k => $conf) {
            $con = new \Redis;

            if ('' != $conf['password']) {
                $rs = $con->connect($conf['host'], $conf['port'], $conf['timeout'], '', 0, 0, ['auth' => [$conf['password']]]);
            } else {
                $rs = $con->connect($conf['host'], $conf['port'], $conf['timeout']);
            }

            if ($rs == true) {
                $this->isConnected = true;
                $this->link[$k] = $con;

                $this->hash->addTarget($k);
            } else {
                $this->isConnected = false;
                /* 跳过 */
                continue;
            }

            if (0 != $conf['select']) {
                $con->select($conf['select']);
            }

            if (!empty($conf['prefix'])) {
                $this->prefix = $conf['prefix'];
            }
        }

        if ($this->isConnected == false) {
            throw new \Exception('redis 连接错误!');
        }
    }

    /**
     * 获取实际的缓存标识
     * @access protected
     * @param  string $name 缓存名
     * @return string
     */
    protected function getCacheKey(string $name) {
        return $this->prefix . $name;
    }

    private function _getConForKey(string $key = '') {
        $i = $this->hash->lookup($key);
        return $this->link[$i];
    }

    /**
     * 检查驱动是否可用
     * @return boolean      是否可用
     */
    public function is_available() {
        if (!$this->isConnected && $this->reConnected < $this->maxReConnected) {

            $this->connect();

            if (!$this->isConnected) {
                $this->reConnected++;
            } else {
                //如果重连成功,重连次数置为0
                $this->reConnected = 0;
            }
        }
        return $this->isConnected;
    }

    /**
     * 设置value,用于序列化存储
     * @param mixed $value
     * @return mixed
     */
    public function setValue($value) {
        if (is_numeric($value)) {
            return $value;
        }
        try {
            $value = serialize($value);
        } catch (\Exception $exc) {
            return false;
        }
        return $value;
    }

    /**
     * 获取value,解析可能序列化的值
     * @param mixed $value
     * @return mixed
     */
    public function getValue($value, $default = false) {
        if (is_null($value) || $value === false) {
            return false;
        }
        if (is_numeric($value)) {
            return $value;
        }

        try {
            $value = unserialize($value);
        } catch (\Exception $exc) {
            return $default;
        }
        return $value;
    }

    /**
     * 设置缓存分组
     * @param type $group
     * @return $this
     */
    public function group_ver(string $group = '_cache_') {
        $this->group = $group;

        $key = $this->getCacheKey('cache_ver_' . $this->group);

        try {
            /* 获取版本号 */
            $this->ver = $this->_getConForKey($key)->get($key);
            if ($this->ver) {
                return $this;
            }
            /* 设置新版本号 */
            $this->ver = $this->_getConForKey($key)->incrby($key, 1);
        } catch (\Exception $ex) {
            //连接状态置为false
            $this->isConnected = false;
            $this->is_available();
            /* 出错 */
            $this->ver = 0;
        }

        return $this;
    }

    /**
     * 按分组清空缓存
     * @param string $group
     * @return type
     * @return boolean
     */
    public function group_clear() {
        if ($this->group) {

            $key = $this->getCacheKey('cache_ver_' . $this->group);
            try {
                /* 获取新版本号 */
                $this->ver = $this->_getConForKey($key)->incrby($key, 1);

                /* 最大版本号修正 */
                if ($this->ver == PHP_INT_MAX) {
                    $this->ver = 1;
                    $this->_getConForKey($key)->set($key, 1);
                }

                return $this->ver;
            } catch (\Exception $ex) {
                //连接状态置为false
                $this->isConnected = false;
                $this->is_available();
            }
        }

        return true;
    }

    /**
     * 获取有分组的缓存
     * @access public
     * @param string $cache_id 缓存变量名
     * @param mixed  $default 默认值
     * @return mixed
     */
    public function group_get(string $cache_id, $default = false) {

        $key = $this->getCacheKey("{$this->group}_{$cache_id}");

        try {

            $value = $this->_getConForKey($key)->get($key);

            if (is_null($value) || false === $value) {
                return $default;
            }

            $data = $this->getValue($value, $default);

            if ($data && $data['ver'] == $this->ver) {
                return $data['data'];
            }
        } catch (\Exception $ex) {
            //连接状态置为false
            $this->isConnected = false;
            $this->is_available();
        }
        return false;
    }

    /**
     * 设置有分组的缓存
     * @param type $cache_id    缓存 key
     * @param type $var         缓存值
     * @param type $ttl      有效期(秒)
     * @return boolean
     */
    public function group_set(string $cache_id, $var, $ttl = 0) {
        $key = $this->getCacheKey("{$this->group}_{$cache_id}");

        /* 缓存数据 */
        $data = $this->setValue(['ver' => $this->ver, 'data' => $var]);

        try {
            if ($ttl == 0) {
                // 缓存 15 ~ 18 天
                $ttl = random_int(1296000, 1555200);
                return $this->_getConForKey($key)->setex($key, $ttl, $data);
            } else {
                // 有时间限制
                return $this->_getConForKey($key)->setex($key, $ttl, $data);
            }
        } catch (\Exception $ex) {
            //连接状态置为false
            $this->isConnected = false;
            $this->is_available();
        }
        return false;
    }

    /**
     * 删除有分组的缓存
     * @param string $cache_id
     * @return bool
     */
    public function group_delete(string $cache_id): bool {
        $key = $this->getCacheKey("{$this->group}_{$cache_id}");

        try {
            $this->_getConForKey($key)->del($key);
            return true;
        } catch (\Exception $ex) {
            //连接状态置为false
            $this->isConnected = false;
            $this->is_available();
        }
        return false;
    }

    /**
     * 简单设置缓存
     * @param string    $cache_id       缓存key
     * @param mix       $var            缓存值
     * @param int       $ttl            有效期(秒)
     * @return bool
     */
    public function set(string $cache_id, $var, int $ttl = 0): bool {
        $key = $this->getCacheKey($cache_id);
        $var = $this->setValue($var);

        if ($this->is_available()) {
            if ($ttl == 0) {
                return $this->_getConForKey($key)->set($key, $var);
            } else {
                // 有时间限制
                return $this->_getConForKey($key)->setex($key, $ttl, $var);
            }
        }
        return false;
    }

    /**
     * 简单设置缓存，不存在时设置成功
     * @param string    $cache_id       缓存key
     * @param mix       $var            缓存值
     * @param int       $ttl            有效期(5秒)
     * @return bool
     */
    public function setnx(string $cache_id, $var, int $ttl = 5) {
        $key = $this->getCacheKey($cache_id);
        $var = $this->setValue($var);

        if ($this->is_available()) {
            return $this->_getConForKey($key)->set($key, $var, ['nx', 'ex' => $ttl]);
        }

        return false;
    }

    /**
     * 简单获取缓存
     * @param string    $cache_id           缓存名称
     * @param bool      $default            默认返回　false
     * @return boolean|int|array
     */
    public function get(string $cache_id, $default = false) {
        $key = $this->getCacheKey($cache_id);
        if ($this->is_available()) {
            $value = $this->_getConForKey($key)->get($key);
            if (is_null($value) || false === $value) {
                return $default;
            }
            return $this->getValue($value, $default);
        }
        return false;
    }

    /**
     * 简单删除缓存(异步)
     * @param type $cache_id
     * @return boolean/int
     */
    public function delete(string $cache_id) {
        $key = $this->getCacheKey($cache_id);
        if ($this->is_available()) {
            return $this->_getConForKey($key)->unlink($key);
        }
        return false;
    }

    /**
     * 自增缓存（针对数值缓存）
     * @param  string    $cache_id 缓存变量名
     * @param  int       $step 步长
     * @return false|int
     */
    public function inc(string $cache_id, int $step = 1) {
        $key = $this->getCacheKey($cache_id);
        return $this->_getConForKey($key)->incrby($key, $step);
    }

    /**
     * 自减缓存（针对数值缓存）
     * @param  string    $cache_id 缓存变量名
     * @param  int       $step 步长
     * @return false|int
     */
    public function dec(string $cache_id, int $step = 1) {
        $key = $this->getCacheKey($cache_id);
        return $this->_getConForKey($key)->decrby($key, $step);
    }

    /**
     * 设置 key(只针对 KV 类型) 的存活时间
     * @param string $cache_id
     * @param int $ttl
     * @return bool
     */
    public function expire(string $cache_id, int $ttl): bool {
        $key = $this->getCacheKey($cache_id);

        if ($this->is_available()) {
            return $this->_getConForKey($key)->expire($key, $ttl);
        }
        return false;
    }

    /**
     * 检查 key 是否存在
     * @param string $cache_id
     * @return bool
     */
    public function exists(string $cache_id): bool {
        $key = $this->getCacheKey($cache_id);

        $rs = 0;
        if ($this->is_available()) {
            $rs = $this->_getConForKey($key)->exists($key);
        }
        return $rs ? true : false;
    }

    /**
     * 返回 key(只针对 KV 类型) 的存活时间
     * @param string $cache_id
     * @return int
     */
    public function ttl(string $cache_id): int {
        $key = $this->getCacheKey($cache_id);

        if ($this->is_available()) {
            return $this->_getConForKey($key)->ttl($key);
        }
        return 0;
    }

    /**
     * 最好能保证它能最后析构!
     * 关闭连接
     */
    public function __destruct() {
        if (!empty($this->link)) {
            foreach ($this->link as $key => $value) {
                $this->link[$key]->close();
            }
        }
        unset($this->link);
        unset($this->isConnected);
    }

}
